iOS 开发:KVC 与 KVO

[TOC]

KVC

Key-Value Coding 基本原则

访问对象属性

1
2
3
4
5
6
7
@interface BankAccount: NSobject

@property (nonatomic) NSNumber *currentBalance; // An attribute
@property (nonatomic) Person *owner; // A to-one relation
@property (nonatomic) NSArray <Transaction *>*transactions; // A to-many relation

@end

currentBalance/owner/transactions 都是 BankAccount 的属性。owner 属性是一个对象,和BankAccount构成一对一的关系,owner对象中的属性改变后并不会影响到owner本身。

为了保持封装,对象通常为其接口上的属性提供访问器方法(accessor methods)。在使用访问器方法时必须在编译之前将属性名称写入代码中。访问器方法的名称成为使用它的代码的静态部分。例如: [myAccount setCurrentBalance:@(100.0)]; 这样缺乏灵活性,KVC提供了使用字符串标识符访问对象属性的更通用的机制。

使用 key 和 keyPath 标识对象的属性

key: 标识特定属性的字符串。通常表示属性的 key 是代码中显示的属性本身的名称。 key 必须使用ASCII 编码,可能不包含空格,并且通常是以小写字母开头(URL 除外)。 上面的赋值过程使用 KVC 表示: [myAccount setValue:@(100.0) forKey:@"currentBalance"];

keyPath: 用来指定要遍历的对象属性序列的一串使用 “.” 分隔的 key。序列中的第一个键的属性是相对于接受者的,并且每个后续键是相对于前一个属性的值的。当需要使用一个方法来向下逐级获取对象层次结构时,keyPath 特别有用。 例如,owner.address.street 应用于银行账户实例的keyPath 是指存储在银行账户所有者地址中的 street 字符串的值。

访问集合属性

符合键值编码的对象以与公开其他属性相同的方式公开其多对多属性。您可以使用 valueForKey:setValue:forKey: 来获取或设置集合属性。但是,当你想要操作这些集合内容的时候,使用协议定义的可变代理方法通常是最有效的。 该协议为集合对象访问定义了三种不同的代理方法,每种方法都有一个key和key path变量:

  • mutableArrayValueForKey:mutableArrayValueForKeyPath: 返回一个行为类似NSMutableArray的代理对象
  • mutableSetValueForKey:mutableSetValueFOrKeyPath: 返回一个行为类似NSMutableSet的代理对象
  • mutableOrderedSetValueForKey:mutableOrderedSetValueForKeyPath: 返回一个行为类似 NSMutableOrderedSet 的代理对象 当您对代理对象进行操作,向对象添加元素,从中删除元素或者替换其中的元素时,协议的默认实现会相应地修改基础属性。这比使用 valueForKey: 获取一个不可变的集合对象,再创建一个可修改的集合,然后把修改后的集合通过 setValue:forKey: 更有效。在许多情况下,它比直接使用可变属性也是更有效的。这些方法为持有集合对象的对象们提供了维护 KVO 特性的好处。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- (void)accessingCollectionProperties {
Transaction *t1 = [[Transaction alloc] init];
Transaction *t2 = [[Transaction alloc] init];
Account *myAccount = [[Account alloc] init];

[myAccount addObserver:self forKeyPath:@"transactions" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:nil];


[myAccount setValue:@[t1, t2] forKey:@"transactions"];
NSLog(@"1st transactions = %@", myAccount.transactions); // 1st transactions = ("<Transaction: 0x6000009d1400>","<Transaction: 0x6000009d1420>")
NSMutableArray <Transaction *>*transactions = [myAccount mutableArrayValueForKey:@"transactions"];

[transactions addObject:[[Transaction alloc] init]];
NSLog(@"2nd transactions = %@", myAccount.transactions); // 2nd transactions = ("<Transaction: 0x6000009d1400>","<Transaction: 0x6000009d1420>","<Transaction: 0x6000009cabf0>")

[transactions removeLastObject];
NSLog(@"3th transactions = %@", myAccount.transactions); // 3th transactions = ("<Transaction: 0x6000009d1400>","<Transaction: 0x6000009d1420")
}
使用集合操作符

当您向 valueForKeyPath: 消息发送符合键值编码的对象时,可以在 keyPath 中嵌入集合运算符。集合运算符是一个小的关键字列表之一,前面是一个 @ 符号,它指定了 getter 应该执行的操作,以便在返回之前以某种方式操作数据。NSObjectvalueForKeyPath: 提供了默认实现。 当 keyPath 包含集合运算符时,运算符之前的部分称为左键路径,指示相对于消息接受者操作的集合,当你直接向一个集合(例如 NSArray)发送消息时左键路径或许可以省略。操作符之后的部分称为右键路径,指定操作符应处理的集合中的属性,除了 @count 之外的所有操作符都需要一个右键路径。

集合运算符表现出三种基本类型的行为:

  • 聚合运算符以某种方式合并集合的对象,并返回通常与右键路径中指定的属性的数据类型匹配的单个对象。@count 是一个例外,它没有正确的关键路径并始终将返回一个 NSNumber 实例。包括:@avg/@count/@max/@min/@sum
  • 数组运算符返回一个 NSArray 实例,该实例包含命名集合中保存的对象的某个子集。包含:@distinctUnionOfObjects/@unionOfObjects
  • 嵌套运算符处理包含其他集合的集合,并根据操作符返回一个 NSArrayNSSet 实例,它以某种方式组合嵌套集合的对象。包含:@distinctUnionOfArrays/@unionOfArrays/@distinctUnionOfSets
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
- (void)usingCollectionOperators {
Transaction *t1 = [Transaction transactionWithPayee:@"Green Power" amount:@(120.00) date:[NSDate dateWithTimeIntervalSinceNow:100]];
Transaction *t3 = [Transaction transactionWithPayee:@"Green Power" amount:@(170.00) date:[NSDate dateWithTimeIntervalSinceNow:300]];
Transaction *t5 = [Transaction transactionWithPayee:@"Car Loan" amount:@(250.00) date:[NSDate dateWithTimeIntervalSinceNow:500]];
Transaction *t6 = [Transaction transactionWithPayee:@"Car Loan" amount:@(250.00) date:[NSDate dateWithTimeIntervalSinceNow:600]];
Transaction *t13 = [Transaction transactionWithPayee:@"Animal Hospital" amount:@(600.00) date:[NSDate dateWithTimeIntervalSinceNow:500]];

NSArray *transactions = @[t1, t3, t5, t6, t13];

/* 聚合运算符
* 聚合运算符可以处理数组或属性集,从而生成反映集合某些方面的单个值。
*/
// @avg 平均值
NSNumber *transactionAverage = [transactions valueForKeyPath:@"@avg.amount"];
NSLog(@"transactionAverage = %@", transactionAverage); // transactionAverage = 278
// @count 个数
NSNumber *numberOfTransactions = [transactions valueForKeyPath:@"@count"];
NSLog(@"numberOfTransactions = %@", numberOfTransactions); // numberOfTransactions = 5
// @max 最大值 使用compare:进行比较
NSDate *latestDate = [transactions valueForKeyPath:@"@max.date"];
NSLog(@"latestDate = %@", latestDate); // latestDate = Thu Nov 1 15:05:59 2018
// @min 最小值 使用compare:进行比较
NSDate *earliestDate = [transactions valueForKeyPath:@"@min.date"];
NSLog(@"earliestDate = %@", earliestDate);// earliestDate = Thu Nov 1 14:57:39 2018
// @sum 总和
NSNumber *amountSum = [transactions valueForKeyPath:@"@sum.amount"];
NSLog(@"amountSum = %@", amountSum); // amountSum = 1390

/* 数组运算符
*
* 数组运算符导致valueForKeyPath:返回与右键路径指示的特定对象集相对应的对象数组。
* 如果使用数组运算符时任何叶对象为nil,则valueForKeyPath:方法会引发异常。
**/
// @distinctUnionOfObjects 创建并返回一个数组,该数组包含与右键路径指定的属性对应的集合的不同对象。会删除重复对象。
NSArray *distinctPayees = [transactions valueForKeyPath:@"@distinctUnionOfObjects.payee"];
NSLog(@"distinctPayees = %@", distinctPayees); // distinctPayees = ("Green Power", "Animal Hospital", "Car Loan")

// @unionOfObjects 创建并返回一个数组,该数组包含与右键路径指定的属性对应的集合的所有对象。不删除重复对象
NSArray *payees = [transactions valueForKeyPath:@"@unionOfObjects.payee"];
NSLog(@"payees = %@", payees); // payees = ("Green Power", "Green Power", "Car Loan", "Car Loan", "Animal Hospital")

/** 嵌套运算符
*
* 嵌套运算符对嵌套集合进行操作,集合中的每个条目都包含一个集合。
* 如果使用数组运算符时任何叶对象为nil,则valueForKeyPath:方法会引发异常。
**/
Transaction *moreT1 = [Transaction transactionWithPayee:@"General Cable - Cottage" amount:@(120.00) date:[NSDate dateWithTimeIntervalSinceNow:10]];
Transaction *moreT2 = [Transaction transactionWithPayee:@"General Cable - Cottage" amount:@(1550.00) date:[NSDate dateWithTimeIntervalSinceNow:3]];
Transaction *moreT7 = [Transaction transactionWithPayee:@"Hobby Shop" amount:@(600.00) date:[NSDate dateWithTimeIntervalSinceNow:160]];
NSArray *moreTransactions = @[moreT1, moreT2, moreT7];
NSArray *arrayOfArrays = @[transactions, moreTransactions];
// @distinctUnionOfArrays 指定@distinctUnionOfArrays运算符时,valueForKeyPath:创建并返回一个数组,该数组包含与右键路径指定的属性对应的所有集合的组合的不同对象。
NSArray *collectedDistinctPayees = [arrayOfArrays valueForKeyPath:@"@distinctUnionOfArrays.payee"];
NSLog(@"collectedDistinctPayees = %@", collectedDistinctPayees); // collectedDistinctPayees = ( "General Cable - Cottage", "Animal Hospital", "Hobby Shop", "Green Power", "Car Loan")
// @unionOfArrays 与@distinctUnionOfArrays 不同的是不会删除相同的元素
NSArray *collectedPayees = [arrayOfArrays valueForKeyPath:@"@unionOfArrays.payee"];
NSLog(@"collectedPayees = %@", collectedPayees); // collectedPayees = ("Green Power", "Green Power", "Car Loan", "Car Loan", "Animal Hospital", "General Cable - Cottage", "General Cable - Cottage", "Hobby Shop")

// @distinctUnionOfSets 与@distinctUnionOfArrays作用相同,只是用于NSSet对象而不是NSArray
}
  • 获取数组里的“最大、最小、平均、求和”
1
2
3
4
5
6
7
8
9
NSArray *array = @[@"1",@"3",@2,@9.5,@"1.2"]; 
NSNumber *sum = [array valueForKeyPath:@"@sum.floatValue"];
NSNumber *avg = [array valueForKeyPath:@"@avg.floatValue"];
NSNumber *max = [array valueForKeyPath:@"@max.floatValue"];
NSNumber *min = [array valueForKeyPath:@"@min.floatValue"];
NSLog(@"sum:%@",sum);
NSLog(@"avg:%@",avg);
NSLog(@"max:%@",max);
NSLog(@"min:%@",min);
  • 删除重复数据
1
2
3
NSArray *array = @[@"name", @"w", @"aa", @"zxp", @"aa"]; //返回的是一个新的数组
NSArray *newArray = [array valueForKeyPath:@"@distinctUnionOfObjects.self"];
NSLog(@"%@", newArray);
  • 同样可以嵌套使用,先剔除 name 对应值的重复数据再取值
1
2
3
4
5
6
NSArray *array = @[ @{@"title":@"zxp",@"name":@"zhangxiaoping"}, @{@"title":@"zxp2",@"name":@"zhangxiaoping2"}, @{@"title":@"zxp",@"name":@"zhangxiaoping3"}, @{@"title":@"zxp",@"name":@"zhangxiaoping"}];
//根据name字段,来进行重复删除。
NSArray *newArray = [array valueForKeyPath:@"@distinctUnionOfObjects.name"];
//如果要根据title字段来删除重名的写法为`@distinctUnionOfObjects.title`
NSLog(@"%@", newArray);
/* print:( zhangxiaoping3, zhangxiaoping2, zhangxiaoping)是一个字符串数组 */
  • 进行实例方法的调用
1
2
NSArray *array = @[@"name", @"w", @"aa", @"ZXPing"]; 
NSLog(@"%@", [array valueForKeyPath:@"uppercaseString"]);

相当于数组中的每个成员执行了uppercaseString方法,然后把返回的对象组成一个新数组返回。既然可以用uppercaseString方法,那么NSString的其他方法也可以,比如[array valueForKeyPath:@"length"]。当然,其他对象的实例方法也可以以此类推来进行调用~!

访问者搜索模式

NSObject 提供的 NSkeyValueCoding 协议的默认实现使用明确定义的规则集将基于键的访问器调用映射到对象的基础属性。这些协议方法使用 “key” 在其自己的对象实例中搜索访问器、实例变量以及遵循某个命名规则的相关方法。虽然您很少修改此默认搜索,但了解它的工作方式会有所帮助,既可以跟踪键值编码对象的行为,也可以使您自己的对象兼容 KVC。

Getter 的搜索模式

valueForKey: 的默认实现是,给定 key 参数作为输入,通过下面的过程,在接收valueForKey: 调用的类实例中操作。

  1. 按顺序搜索访问器方法 get<Key>/<key>/is<Key>/_<key>。如果找到,调用该方法并且带着方法的调用结果调转到第5步执行;否则,继续下一步。
  2. 如果没有找到简单的访问方法,搜索其名称匹配某些模式的方法的实例。其中匹配模式包含countOf<Key>objectIn<Key>AtIndex:(对应于 NSArray 定义的基本方法),和<key>AtIndexs:(对应于 NSArray 的方法 objectsAtIndexs:) 一旦找到第一个和其他两个中的至少一个,则创建一个响应所以 NSArray 方法并返回该方法的集合代理对象。否则,执行第3步。 代理对象随后将任何NSArray接收到的一些组合的消息。**实际上,与符合键值编码对象一起工作的代理对象允许底层属性的行为就像它是 NSArray 一样,即便它不是。
  3. 如果没有找到简单的访问器方法或数组访问方法组,则寻找三个方法countOf<Key>/enumeratorOf<Key>/memberOf<Key>:,对应 NSSet 类的基本方法。 如果三个方法全找到了,则创建一个集合代理对象来响应所有的NSSet方法并返回。否则,执行第4步。
  4. 如果上面的方法都没有找到,并且接受者的类方法 accessInstanceVariablesDirectly 返回 YES(默认 YES),则按序搜索以下实例变量:_<key>/_is<Key>/<key>/is<Key>。如果找到其中之一,直接获取实例变量的值并跳转到第5步;否则执行第6步。
  5. 如果检索到的属性值是对象指针,则只返回结果;如果值是受 NSNumber 支持的标量,则将其存储在 NSNumber 实例中并返回;如果结果是 NSNumber 不支持的标量,则转换成 NSValue 对象并返回
  6. 如果以上所有的尝试都失败了,则调用 valueForUndefinedKey:,这个方法默认抛出异常,NSObject 的子类可以重写来自定义行为。
Setter 的搜索模式

setValue:forKey: 的默认实现是给定 keyvalue 作为参数输入,尝试把 value 设置给以 key 命名的属性。过程如下:

  1. 按序搜索 set<Key>:_set<Key>,如果找到,则使用输入参数调用并结束。
  2. 如果没有找到简单的访问器方法,并且如果类方法 accessInstanceVariablesDirectly 返回 YES(默认为 YES),则按序搜索以下实例变量: _<key>/_is<Key>/<key>/is<Key>,如果找到了则直接进行赋值并结束。
  3. 以上方法皆失败则调用 setValue:forUndefinedKey:,这个方法默认抛出异常,NSObject 的子类可以自定义。

KVO

Key-Value Observing 提供了一种机制,允许对象把自身属性的更改通知给其他属性。它对应用程序中 Model 和Controller 层之间的通信特别有用。通常,控制器对象观察模型对象的属性,视图对象通过控制器观察模型对象的属性。另外,一个模型对象或许会观察另一个模型对象(通常用与确认从属值何时改变)或甚至自身(再次确认从属值何时改变)。 你可以观察属性,包括简单属性,一对一关系和多对多关系。多对多关系的观察者被告知所作出的改变的类型——以及改变中涉及哪些对象。

注册 KVO

  • 使用addObserver:forKeyPath:options:content:方法来给observer注册一个observed object
  • 在observer内部实现observerValueForKeyPath:ofObject:change:context:来接收更改的通知消息。
  • 当不再应该接收消息时,使用removeObserver:forKeyPath:方法来反注册观察者。起码也要在observer被移除前调用这个方法。

兼容KVO

为了让特定属性符合 KVO 标准,class 必须满足一下内容:

  • 该类必须是符合该属性的 KVC
  • 该类会为该属性触发 KVO 通知
  • 相关的key已经被成功注册

有两种技术可确保发出 KVO 通知。NSObject 提供自动支持,默认情况下可用于符合键值编码的类的所有属性。通常,如果你遵守 Cocoa 编码和命名约定,则可以使用自动通知,而不必编写任何代码。

手动方式为通知触发时提供了更多的控制权,并且需要额外编码。你可以通过实现 automaticallyNotifiesObserversForKey: 来控制子类属性的自动通知。

注册从属keys

在许多情况下,一个属性的值取决于另一个对象中的一个或多个其他属性的值。如果一个属性的值发生更改,则还应标记派生属性的值以进行更改。

Key-Value Observing 的实现细节

自动 key-value observing 是使用一种叫做 isa-swizzling 的技术实现的。

isa 指针指向维护一个调度表(dispatch table)的对象的类。该调度表包含了指向该类实现的方法的指针,以及其他数据。

当观察者注册对象的属性时,观察对象的 isa 指针被修改,指向中间类而不是真正的类。因此,isa 指针的值不一定反映实例的实际类。

永远不要依赖 isa 指针来确定类成员。而应该使用 class 方法来决定实例所属的类。